package com.atguigu.gmall.seckill.service.impl;

import com.alibaba.fastjson.JSON;
import com.atguigu.gmall.common.config.mq.service.MqService;
import com.atguigu.gmall.common.config.utils.UserAuthUtil;
import com.atguigu.gmall.common.constant.MqConst;
import com.atguigu.gmall.common.constant.RedisConst;
import com.atguigu.gmall.common.execption.GmallException;
import com.atguigu.gmall.common.result.ResultCodeEnum;
import com.atguigu.gmall.common.util.DateUtil;
import com.atguigu.gmall.common.util.MD5;
import com.atguigu.gmall.feign.order.OrderFeignClient;
import com.atguigu.gmall.msg.SeckillOrderQueueMsg;
import com.atguigu.gmall.order.entity.OrderInfo;
import com.atguigu.gmall.order.vo.OrderSubmitVo;
import com.atguigu.gmall.seckill.entity.SeckillGoods;
import com.atguigu.gmall.seckill.service.SeckillBizService;
import com.atguigu.gmall.seckill.service.SeckillGoodsService;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @Description
 * @Return
 * @Author https://weiranyi.github.io
 * @Date Created in 2022-12-26 10:48
 **/
@Slf4j
@Service
public class SeckillBizServiceImpl implements SeckillBizService {
    @Autowired
    SeckillGoodsService seckillGoodsService;
    @Autowired
    StringRedisTemplate redisTemplate;
    @Autowired
    MqService mqService;
    @Autowired
    OrderFeignClient orderFeignClient;

    // 设计一个本地缓存【启动几个微服务就有几个本地缓存】
    Map<Long, SeckillGoods> localCache = new ConcurrentHashMap<>();


    @Override
    public void upSeckillGoods(String date) {
        // 1.查询当天需要秒杀的商品
        List<SeckillGoods> todaySeckillGoods = seckillGoodsService.getTodaySeckillGoods(date);
        todaySeckillGoods.stream().forEach(goods -> {
            // 2.上架到redis
            redisTemplate.opsForHash().put(RedisConst.SECKILL_GOODS_CACHE + date, goods.getSkuId().toString(), JSON.toJSONString(goods));
            // 4.缓存到一级缓存中
            localCache.put(goods.getSkuId(), goods);
        });
        // 3.设置过期时间,数据多缓存一天时为了盘账
        redisTemplate.expire(RedisConst.SECKILL_GOODS_CACHE + date, 2, TimeUnit.DAYS);

    }

    /**
     * 得到当天的可秒杀的商品
     *
     * @param date
     * @return
     */
    @Override
    public List<SeckillGoods> getTodaySeckillGoods(String date) {
        // 1.1 先找一级缓存
        List<SeckillGoods> objs = localCache.values().stream().collect(Collectors.toList());
        // 1.2 判断一级缓存是否有值
        if (objs == null || objs.size() == 0) {
            // 2.1 一级没有找到缓存，再找二级缓存
            List<Object> values = redisTemplate.opsForHash().values(RedisConst.SECKILL_GOODS_CACHE + date);
            // 2.2 判断二级缓存是否有数值
            if (values == null || values.size() == 0) {
                // 2.3 二级缓存也没有【走到这，代表今天没有参与秒杀的商品，因为在这之前定时任务已经回源过了】
                return null;
            }
            // 2.4 二级缓存命中的情况
            log.info("二级缓存命中");
            // 2.5 将redis得到的数据集转为List<SeckillGoods>
            List<SeckillGoods> goods = values.stream()
                    .map(item -> JSON.parseObject(item.toString(), SeckillGoods.class))
                    .sorted(Comparator.comparing(SeckillGoods::getStartTime))
                    .collect(Collectors.toList());
            // 2.6 同步到本地内存（原因：可能断电导致一级缓存丢失，二级缓存有数据的情况）
            goods.stream().forEach(item -> {
                localCache.put(item.getSkuId(), item);
            });
            return goods;
        } else {
            // 1.3 一级缓存命中
            log.info("一级缓存命中");
            return objs;
        }
    }

    @Override
    public SeckillGoods getSeckillGoodsDetails(Long skuId) {
        // 1.1 从本地缓存中得到秒杀商品详情
        SeckillGoods seckillGoods = localCache.get(skuId);
        // 1.2 如果得到商品详情，立马返回
        if (seckillGoods != null) {
            log.info("一级缓存命中---得到详情");
            return seckillGoods;
        }
        log.info("正在同步二级缓存到一级中");
        // 2.1 【同步代码】防止一级缓存没有数据-该代码运行后将二级缓存数据同步到一级缓存
        String date = DateUtil.formatDate(new Date(), "yyyy-MM-dd");
        getTodaySeckillGoods(date);
        // 3.再次从一级缓存获取数据
        seckillGoods = localCache.get(skuId);
        return seckillGoods;
    }

    @Override
    public String generateSeckillCode(Long skuId) {
        // 1.校验：每次生成秒杀码之前先校验【用户必须登录-网关做了；商品在秒杀状态；商品还有库存】===================
        // 1.1 验证用户登录与否
        String userIdHeader = UserAuthUtil.getUserIdHeader();
        if (userIdHeader == null) {
            throw new GmallException(ResultCodeEnum.LOGIN_AUTH);
        }
        // 1.2 秒杀是否开始了
        SeckillGoods details = getSeckillGoodsDetails(skuId);
        Date currentDate = new Date();
        Date startTime = details.getStartTime();
        Date endTime = details.getEndTime();
        if (startTime.after(currentDate)) {
            throw new GmallException(ResultCodeEnum.SECKILL_NO_START);
        }
        // 1.3 秒杀是否结束了
        if (endTime.before(currentDate)) {
            throw new GmallException(ResultCodeEnum.SECKILL_END);
        }
        // 1.4 验证库存【内存库存状态位】
        if (details.getStockCount() <= 0) {
            //  布隆过滤器-只有内存中没有就一定没有
            throw new GmallException(ResultCodeEnum.SECKILL_FINISH);
        }
        //=======================================================================================
        // 2.1生成一个码，前端一份，redis一份【同一用户 同一商品 同一天只能秒杀一次】
        String userId = UserAuthUtil.getUserIdHeader();
        String date = DateUtil.formatDate(currentDate, "yyyy-MM-dd");
        String code = MD5.encrypt(skuId + "_" + userId + "_" + date);
        // 2.2 保存code【避免用户在秒杀详情页重新点击立即抢购获得新秒杀码，覆盖掉第一次点击时获取的码，导致seckillOrder中统计逻辑失效】
        redisTemplate.opsForValue().setIfAbsent(RedisConst.SECKILL_CODE_TOKEN_CACHE + code, "0", 1, TimeUnit.DAYS);
        return code;
    }


    @Override
    public void seckillOrder(String skuId, String code) {
        // 1.校验：判断是否登录，是否再秒杀时间内，是否还有库存===================
        // 1.1 验证用户登录与否
        String userIdHeader = UserAuthUtil.getUserIdHeader();
        if (userIdHeader == null) {
            throw new GmallException(ResultCodeEnum.LOGIN_AUTH);
        }
        // 1.2 秒杀是否开始了
        SeckillGoods details = getSeckillGoodsDetails(Long.parseLong(skuId));
        Date currentDate = new Date();
        Date startTime = details.getStartTime();
        Date endTime = details.getEndTime();
        if (startTime.after(currentDate)) {
            throw new GmallException(ResultCodeEnum.SECKILL_NO_START);
        }
        // 1.3 秒杀是否结束了
        if (endTime.before(currentDate)) {
            throw new GmallException(ResultCodeEnum.SECKILL_END);
        }

        // 1.4生成算法校验【双校验I-保证带的自己的码】
        String date = DateUtil.formatDate(currentDate, "yyyy-MM-dd");
        String seckillCode = MD5.encrypt(skuId + "_" + UserAuthUtil.getUserIdHeader() + "_" + date);
        if (!seckillCode.equals(code)) {
            // 前端代的是错误的秒杀码
            throw new GmallException(ResultCodeEnum.SECKILL_ILLEGAL);
        }
        // 1.5 redis【双校验II-保证是服务器生成的】
        Boolean hasKey = redisTemplate.hasKey(RedisConst.SECKILL_CODE_TOKEN_CACHE + code);
        if (!hasKey) {
            throw new GmallException(ResultCodeEnum.SECKILL_ILLEGAL);
        }
        // ===========================================================================
        // 1.6 控制每个用户确实只发送了一次排队消息
        Long increment = redisTemplate.opsForValue().increment(RedisConst.SECKILL_CODE_TOKEN_CACHE + code);
        if (increment > 1) {
            // 1.7 到这-说明已经发过请求，返回ok，用户每3s查一次订单
            log.info("用户多次发起了秒杀请求，已排队，无需再发送消息；controller给用户返回ok，用户前端每隔每3s查一次订单");
        } else {
            log.info("用户发起了一次秒杀请求-开始，剩余商品数【{}】", details.getStockCount());
            details.setStockCount(details.getStockCount() - 1); //修改内存库存
            // 1.8 扣完库存后判断还有剩余库存不【内存库存状态位】
            if (details.getStockCount() < 0) {
                //  布隆过滤器-只有内存中没有就一定没有
                throw new GmallException(ResultCodeEnum.SECKILL_FINISH);
            }
            // 1.9 【扣真库存的逻辑开始】到这-代表所有校验都通过啦，咋门可能存在库存；尽量携带更多的信息 userId，skuId，code
            SeckillOrderQueueMsg seckillOrderQueueMsg = new SeckillOrderQueueMsg();
            seckillOrderQueueMsg.setUserId(Long.parseLong(userIdHeader));
            seckillOrderQueueMsg.setSkuId(Long.parseLong(skuId));
            seckillOrderQueueMsg.setCode(code);
            mqService.convertAndSend(MqConst.SECKILL_EVENT_EXCHANGE, MqConst.SECKILL_QUEUE_RK, seckillOrderQueueMsg);
        }
    }

    @Override
    public ResultCodeEnum checkOrder(Long skuId, String userId) {
        // 1.判断有没有秒杀单
        // 得到秒杀码
        String date = DateUtil.formatDate(new Date(), "yyyy-MM-dd");
        String code = MD5.encrypt(skuId + "_" + userId + "_" + date);
        String json = redisTemplate.opsForValue().get(RedisConst.SECKILL_ORDER_CACHE + code);
        if (StringUtils.isEmpty(json)) {
            // 秒杀请求太多，在MQ中排队，还没有被消费到
            String ordinals = redisTemplate.opsForValue().get(RedisConst.SECKILL_CODE_TOKEN_CACHE + code);
            if (Long.parseLong(ordinals) >= 1) {
                return ResultCodeEnum.SECKILL_RUN; // 【211】 正在排队
            } else {
                return ResultCodeEnum.SECKILL_ILLEGAL; // 请求不合法
            }
        } else {
            if ("x".equals(json)) {
                // 被抢完了 213
                return ResultCodeEnum.SECKILL_FAIL;
            }
            OrderInfo orderInfo = JSON.parseObject(json, OrderInfo.class);
            // 通过是否有收货地址判定是否已经走完了下单流程
            if (orderInfo.getConsignee() == null || "".equals(orderInfo.getConsignee())) {
                // 无收货人信息，刚秒杀 【215】抢单成功
                return ResultCodeEnum.SECKILL_SUCCESS;
            } else {
                // 有收货人信息，下单成功过了 【218】下单成功
                return ResultCodeEnum.SECKILL_ORDER_SUCCESS;
            }

        }
    }

    @Override
    public OrderInfo getSeckillOrderInfo(Long skuId, String userId) {
        // 得到秒杀码
        String date = DateUtil.formatDate(new Date(), "yyyy-MM-dd");
        String code = MD5.encrypt(skuId + "_" + userId + "_" + date);
        // 查询
        String json = redisTemplate.opsForValue().get(RedisConst.SECKILL_ORDER_CACHE + code);
        return JSON.parseObject(json, OrderInfo.class);
    }

    @Override
    public Long submitOrder(OrderSubmitVo vo) {

        Long skuId = vo.getOrderDetailList().get(0).getSkuId();
        String userId = UserAuthUtil.getUserIdHeader();
        OrderInfo rpcData = this.getSeckillOrderInfo(skuId, userId);
        rpcData.setConsignee(vo.getConsignee());
        rpcData.setConsigneeTel(vo.getConsigneeTel());
        rpcData.setDeliveryAddress(vo.getDeliveryAddress());
        rpcData.setOrderComment(vo.getOrderComment());

        // 远程调用订单服务，存订单并且得到订单id
        Long orderId = orderFeignClient.saveOrder(rpcData).getData();
        // 改redis数据
        rpcData.setId(orderId);
        // 得到秒杀码
        String date = DateUtil.formatDate(new Date(), "yyyy-MM-dd");
        String code = MD5.encrypt(skuId + "_" + userId + "_" + date);
        redisTemplate.opsForValue().set(RedisConst.SECKILL_ORDER_CACHE+code,JSON.toJSONString(rpcData));



        return orderId;
    }


}
